package cn.cas.mango.service;

import cn.cas.mango.dto.Message;
import cn.cas.mango.dto.News;
import cn.cas.mango.dto.param.QueryParam;
import cn.cas.mango.service.samenews.SameNewsService;
import cn.cas.mango.util.Pair;
import cn.cas.mango.util.paramCheck.QueryParamCheckUtil;
import cn.cas.mango.util.RequestUtil.BuildRequest;
import cn.cas.mango.util.ResponseUtil.ResponseUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.aggregations.MaxAggregate;
import co.elastic.clients.elasticsearch._types.aggregations.MinAggregate;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class SearchService {
  @Autowired
  private ElasticsearchClient elasticsearchClient;

  @Autowired
  private ConfigService config;
  @Autowired
  private BuildRequest buildRequest;

  private JSONObject addLink2Doc(List<JSONObject> doc,
                                 Hit<News> newsHit, Map<String, List<Hit<News>>> sameNewsMap,
                                 boolean zh) {

    News hitSource = newsHit.source();
    JSONObject link = new JSONObject();
    Map<String, List<String>> highlight = newsHit.highlight();
    assert hitSource != null;
    ResponseUtil.putTitleContent(highlight, link, hitSource, zh);
    ResponseUtil.putCommon(config, link, newsHit);
    link.put("news_publictime", hitSource.getNewsPublicTime());
    link.put("news_website_type", hitSource.getNewsWebsiteType());
    link.put("score", newsHit.score());

    assert newsHit.source() != null;
    if (sameNewsMap.containsKey(newsHit.source().getId())) {
      ArrayList<JSONObject> neighbors = new ArrayList<>();
      List<Hit<News>> newsList = sameNewsMap.get(newsHit.source().getId());
      for (Hit<News> hitNeighbor: newsList) {
        neighbors.add(addLink2Doc(doc, hitNeighbor, sameNewsMap, zh));
      }
      link.put("sameNews", neighbors);
      doc.add(link);
    } else {
      link.put("sameNews",Collections.EMPTY_LIST);
    }
    return link;
  }

  private JSONObject addLink2Doc(List<JSONObject> doc, Hit<News> newsHit, boolean zh) {
    News hitSource = newsHit.source();
    JSONObject link = new JSONObject();

    assert hitSource != null;

    link.put("news_content", zh ? hitSource.getNewsContentZh(): hitSource.getNewsContent());
    link.put("news_title", zh ? hitSource.getNewsTitleZh(): hitSource.getNewsTitle());
    link.put("news_publictime_date", hitSource.getNewsPublicTime());
    link.put("id", hitSource.getId());
    link.put("score", newsHit.score());
    link.put("news_website", hitSource.getNewsWebsite());
    doc.add(link);
    return link;
  }

  private List<Hit<News>> distinctById(List<Hit<News>> list) {
    Map<String, Hit<News>> set = new LinkedHashMap<>();
    for (Hit<News> hit: list) {
      assert hit.source() != null;
      set.put(hit.source().getId(), hit);
    }
    return new ArrayList<>(set.values());
  }


  /**
   * 查询.
   */
  public Message searchAdvance(QueryParam queryParam, String type) throws IOException {
    long start = System.currentTimeMillis();
    Map<String, Object> errMessage = QueryParamCheckUtil.checkParam(queryParam, config.getPageSize());
    String isSuccess = (String) errMessage.get("isSuccess");
    if (!isSuccess.equals("ok")) {
      return Message.unSuccess(isSuccess);
    }
    queryParam = QueryParam.getFromFormData(errMessage);
    QueryParam finalQueryParam = queryParam;

    List<Object> sorts = queryParam.get3Sorts();
    SortOrder order = (SortOrder) sorts.get(0);
    String sortField = (String) sorts.get(1);
    SortOptions sortOptions = (SortOptions) sorts.get(2);
    SearchRequest searchRequest;
    if (queryParam.getSortType().equals("sim")) {
      if (queryParam.getQueryStr().contains("+") || queryParam.getQueryStr().contains("-")
          ) {
        searchRequest = buildRequest.buildQueryStrRequest(queryParam)
            .sort(sortOptions)
            .build();
      } else {
        searchRequest = buildRequest.buildRequest(queryParam)
            .sort(sortOptions)
            .build();
      }
    } else {
      SortOptions scoreOptions = SortOptions.of(
          s -> s.field(f -> f.field("_score").order(SortOrder.Desc)));
      if (queryParam.getQueryStr().contains("+") || queryParam.getQueryStr().contains("-")) {
        searchRequest = buildRequest.buildQueryStrRequest(queryParam)
            .sort(sortOptions, scoreOptions)
            .build();
      } else {
        searchRequest = buildRequest.buildRequest(queryParam)
            .sort(sortOptions, scoreOptions)
            .build();
      }
    }

    log.info("Search Request Json:{}", searchRequest.toString().split("typed_keys=true")[1]);
    log.info("search index: {}", searchRequest.index());
    long searchStart = System.currentTimeMillis();
    SearchResponse<News> searchResponse = elasticsearchClient.search(searchRequest, News.class);
    long searchEnd = System.currentTimeMillis();

    List<JSONObject> doc = new ArrayList<>();
    HitsMetadata<News> hits = searchResponse.hits();
    TotalHits total = hits.total();
    log.info("hit number: {}, search time: {}s", total, (searchEnd - searchStart) / 1000.0);
    List<Hit<News>> list = distinctById(hits.hits());
    if (type.equals("searchAdvanced")) {
      Map<String, List<Hit<News>>> sameNewsMap = new LinkedHashMap<>();
      if (queryParam.getIdsArray().isEmpty()) {
        long clusterStart = System.currentTimeMillis();
        sameNewsMap = new SameNewsService(list, order, sortField).getSameNewsList(queryParam.getParam());
        long clusterEnd = System.currentTimeMillis();
        log.info("show number: {}, cluster time: {}s", sameNewsMap.size(), (clusterEnd - clusterStart) / 1000.0);
      }


      for (Hit<News> newsHit : list) {
        assert newsHit.source() != null;
        if (queryParam.getIdsArray().isEmpty()) {
          if (!sameNewsMap.containsKey(newsHit.source().getId())) {
            continue;
          }
          addLink2Doc(doc, newsHit, sameNewsMap, queryParam.isZh());
        } else {
          doc.add(addLink2Doc(doc, newsHit, queryParam.isZh()));
        }
      }
    } else {
      for (Hit<News> newsHit : list) {
        assert newsHit.source() != null;
        addLink2Doc(doc, newsHit, queryParam.isZh());
      }
    }
    assert total != null;
    long totalValue = total.value();
    JSONObject data = new JSONObject();
    if (type.equals("searchAdvanced")) {
      JSONObject results = new JSONObject();
      results.put("totalResults", totalValue);
      results.put("document", doc);
      results.put("time", System.currentTimeMillis() - start);

      String queryStr = finalQueryParam.getQueryStr();
      if (queryParam.getSearchType().equals("true")) {
        queryStr = "搜索条件：" + queryStr + "（100%命中）";
      } else {
        queryStr = "搜索条件：" + queryStr;
      }
      data.put("queryStr", queryStr);
      data.put("results", results);
      write2History(data, finalQueryParam);
    }  else {
      data.put("currentPage", queryParam.getCurrentPage());
      data.put("pageSize", queryParam.getPageSize());
      data.put("totalResults", totalValue);
      data.put("documents", doc);
    }

    return Message.builder().code(200).message("success.").data(data).build();
  }

  public Message biSearchAdvance(QueryParam queryParam) throws IOException {
    long start = System.currentTimeMillis();
    Map<String, Object> errMessage = QueryParamCheckUtil.checkParam(queryParam, config.getPageSize());
    String isSuccess = (String) errMessage.get("isSuccess");
    if (!isSuccess.equals("ok")) {
      return Message.unSuccess(isSuccess);
    }
    queryParam = QueryParam.getFromFormData(errMessage);
    QueryParam finalQueryParam = queryParam;

    List<Object> sorts = queryParam.get3Sorts();
    SortOrder order = (SortOrder) sorts.get(0);
    String sortField = (String) sorts.get(1);
    SortOptions sortOptions = (SortOptions) sorts.get(2);
    SearchRequest.Builder searchRequestBuilder;
    queryParam.setSourceSearchType("bi");
    if (queryParam.getSortType().equals("sim")) {
      if (queryParam.getQueryStr().contains("+") || queryParam.getQueryStr().contains("-")
      ) {
        searchRequestBuilder = buildRequest.buildQueryStrRequest(queryParam)
            .sort(sortOptions);
      } else {
        searchRequestBuilder = buildRequest.buildRequest(queryParam)
            .sort(sortOptions);
      }
    } else {
      SortOptions scoreOptions = SortOptions.of(
          s -> s.field(f -> f.field("_score").order(SortOrder.Desc)));
      if (queryParam.getQueryStr().contains("+") || queryParam.getQueryStr().contains("-")) {
        searchRequestBuilder = buildRequest.buildQueryStrRequest(queryParam)
            .sort(sortOptions, scoreOptions);
      } else {
        searchRequestBuilder = buildRequest.buildRequest(queryParam)
            .sort(sortOptions, scoreOptions);
      }
    }
    searchRequestBuilder = searchRequestBuilder.aggregations("max_score", max->max.max(s->s.script(f->f.inline(v->v.source("_score")))))
        .aggregations("min_score", min->min.min(s->s.script(f->f.inline(v->v.source("_score")))));

    SearchRequest searchRequest = searchRequestBuilder.build();
    log.info("biSearchAdvance Search Request Json:{}", searchRequest.toString().split("typed_keys=true")[1]);
    log.info("search index: {}", searchRequest.index());
    long searchStart = System.currentTimeMillis();
    SearchResponse<News> searchResponse = elasticsearchClient.search(searchRequest, News.class);
    long searchEnd = System.currentTimeMillis();

    List<JSONObject> doc = new ArrayList<>();
    HitsMetadata<News> hits = searchResponse.hits();
    TotalHits total = hits.total();
    log.info("hit number: {}, search time: {}s", total, (searchEnd - searchStart) / 1000.0);
    List<Hit<News>> list = distinctById(hits.hits());

    Map<String, List<Hit<News>>> sameNewsMap = new LinkedHashMap<>();
    if (queryParam.getIdsArray().isEmpty()) {
      long clusterStart = System.currentTimeMillis();
      sameNewsMap = new SameNewsService(list, order, sortField).getSameNewsList(queryParam.getParam());
      long clusterEnd = System.currentTimeMillis();
      log.info("show number: {}, cluster time: {}s", sameNewsMap.size(), (clusterEnd - clusterStart) / 1000.0);
    }


    for (Hit<News> newsHit : list) {
      assert newsHit.source() != null;
      if (queryParam.getIdsArray().isEmpty()) {
        if (!sameNewsMap.containsKey(newsHit.source().getId())) {
          continue;
        }
        addLink2Doc(doc, newsHit, sameNewsMap, queryParam.isZh());
      } else {
        doc.add(addLink2Doc(doc, newsHit, queryParam.isZh()));
      }

    }


    Pair<Double, Double> minMaxScore = getMinMaxScore(searchResponse);
    double minScore = minMaxScore.getKey();
    double maxScore = minMaxScore.getValue();
    assert total != null;
    long totalValue = total.value();
    JSONObject data = new JSONObject();

    JSONObject results = new JSONObject();
    results.put("totalResults", totalValue);
    results.put("document", doc);
    results.put("min_score", minScore);
    results.put("max_score", maxScore);

    results.put("time", System.currentTimeMillis() - start);

    String queryStr = finalQueryParam.getQueryStr();
    if (queryParam.getSearchType().equals("true")) {
      queryStr = "搜索条件：" + queryStr + "（100%命中）";
    } else {
      queryStr = "搜索条件：" + queryStr;
    }
    data.put("queryStr", queryStr);
    data.put("results", results);
//    write2History(data, finalQueryParam);


    return Message.builder().code(200).message("success.").data(data).build();
  }


  public Message biSearchPerson(QueryParam queryParam) throws IOException {
    long start = System.currentTimeMillis();
    Map<String, Object> errMessage = QueryParamCheckUtil.checkParam(queryParam, config.getPageSize());
    String isSuccess = (String) errMessage.get("isSuccess");
    if (!isSuccess.equals("ok")) {
      return Message.unSuccess(isSuccess);
    }
    queryParam = QueryParam.getFromFormData(errMessage);
    QueryParam finalQueryParam = queryParam;

    List<Object> sorts = queryParam.get3Sorts();
    SortOrder order = (SortOrder) sorts.get(0);
    String sortField = (String) sorts.get(1);
    SortOptions sortOptions = (SortOptions) sorts.get(2);
    SearchRequest.Builder searchRequestBuilder;
    queryParam.setSourceSearchType("bi");
    if (queryParam.getSortType().equals("sim")) {
      searchRequestBuilder = buildRequest.buildBiSearchRequest(queryParam).sort(sortOptions);
    } else {
      SortOptions scoreOptions = SortOptions.of(
          s -> s.field(f -> f.field("_score").order(SortOrder.Desc)));
      searchRequestBuilder = buildRequest.buildBiSearchRequest(queryParam).sort(sortOptions, scoreOptions);
    }

    searchRequestBuilder = searchRequestBuilder.aggregations("max_score", max->max.max(s->s.script(f->f.inline(v->v.source("_score")))))
        .aggregations("min_score", min->min.min(s->s.script(f->f.inline(v->v.source("_score")))));

    SearchRequest searchRequest = searchRequestBuilder.build();
    log.info("biSearchAdvance Search Request Json:{}", searchRequest.toString().split("typed_keys=true")[1]);
    log.info("search index: {}", searchRequest.index());
    long searchStart = System.currentTimeMillis();
    SearchResponse<News> searchResponse = elasticsearchClient.search(searchRequest, News.class);
    long searchEnd = System.currentTimeMillis();

    List<JSONObject> doc = new ArrayList<>();
    HitsMetadata<News> hits = searchResponse.hits();
    TotalHits total = hits.total();
    log.info("hit number: {}, search time: {}s", total, (searchEnd - searchStart) / 1000.0);
    List<Hit<News>> list = distinctById(hits.hits());

    Map<String, List<Hit<News>>> sameNewsMap = new LinkedHashMap<>();
    if (queryParam.getIdsArray().isEmpty()) {
      long clusterStart = System.currentTimeMillis();
      sameNewsMap = new SameNewsService(list, order, sortField).getSameNewsList(queryParam.getParam());
      long clusterEnd = System.currentTimeMillis();
      log.info("show number: {}, cluster time: {}s", sameNewsMap.size(), (clusterEnd - clusterStart) / 1000.0);
    }


    for (Hit<News> newsHit : list) {
      assert newsHit.source() != null;
      if (queryParam.getIdsArray().isEmpty()) {
        if (!sameNewsMap.containsKey(newsHit.source().getId())) {
          continue;
        }
        addLink2Doc(doc, newsHit, sameNewsMap, queryParam.isZh());
      } else {
        doc.add(addLink2Doc(doc, newsHit, queryParam.isZh()));
      }

    }


    Pair<Double, Double> minMaxScore = getMinMaxScore(searchResponse);
    double minScore = minMaxScore.getKey();
    double maxScore = minMaxScore.getValue();
    assert total != null;
    long totalValue = total.value();
    JSONObject data = new JSONObject();

    JSONObject results = new JSONObject();
    results.put("totalResults", totalValue);
    results.put("document", doc);
    results.put("min_score", minScore);
    results.put("max_score", maxScore);

    results.put("time", System.currentTimeMillis() - start);

    String queryStr = finalQueryParam.getQueryStr();
    if (queryParam.getSearchType().equals("true")) {
      queryStr = "搜索条件：" + queryStr + "（100%命中）";
    } else {
      queryStr = "搜索条件：" + queryStr;
    }
    data.put("queryStr", queryStr);
    data.put("results", results);
//    write2History(data, finalQueryParam);


    return Message.builder().code(200).message("success.").data(data).build();
  }

  private Pair<Double, Double> getMinMaxScore(SearchResponse<News> searchResponse) {
    Map<String, Aggregate> aggregations = searchResponse.aggregations();
    Aggregate maxScore = aggregations.get("max_score");
    Aggregate minScore = aggregations.get("min_score");
    double min = 0;
    double max = 0;
    try {
      min = tranScore(minScore);
      max = tranScore(maxScore);
    } catch (Exception e) {
      return new Pair<>(min, max);
    }
    return new Pair<>(min, max);
  }

  private double tranScore(Aggregate score) {
    Object o = score._get();
    if (o instanceof MinAggregate) {
      MinAggregate minAggregate = (MinAggregate) o;
      return minAggregate.value();
    } else {
      MaxAggregate maxAggregate = (MaxAggregate) o;
      return maxAggregate.value();
    }
  }

  private void write2History(JSONObject data,
                             QueryParam finalQueryParam) {
    List<String> histories = new ArrayList<>();
    File file = new File(config.getHistoryPath());
    try {
      if (!file.exists()) {
        if (file.createNewFile()) {
          log.info("历史记录文件创建成功");
        } else {
          log.info("历史记录文件创建失败...");
        }
      }
      List<String> lines = Files.readAllLines(Paths.get(config.getHistoryPath()));
      histories.addAll(lines);
      Object[] historySearch = histories.toArray();
      List<Object> historySearchList = Arrays.asList(historySearch);
      Collections.reverse(historySearchList);
      data.put("history", historySearchList);
      if (histories.size() == 0
          || !histories.get(histories.size() - 1).equals(finalQueryParam.getQueryStr())
          && Strings.isNotBlank(finalQueryParam.getQueryStr())) {
        histories.add(finalQueryParam.getQueryStr());
      }
      if (histories.size() > 10) {
        histories.remove(0);
      }
      OutputStreamWriter osw = new OutputStreamWriter(
          Files.newOutputStream(Paths.get(config.getHistoryPath())),
          StandardCharsets.UTF_8);
      for (String history : histories) {
        osw.write(history + "\n");
      }
      osw.flush();
      osw.close();
    } catch (Exception e) {
      log.error("创建文件失败", e);
    }
  }
}
